﻿//-----------------------------------------------------------------------
// <copyright file="ApplicationDataContainerSettings.cs" company="In The Hand Ltd">
//     Copyright (c) 2013-15 In The Hand Ltd. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
#if WINDOWS_PHONE
using System.IO.IsolatedStorage;
using Windows.Foundation.Collections;
#elif __IOS__
using InTheHand.Foundation.Collections;
using Foundation;
using System.Globalization;
#endif

namespace InTheHand.Storage
{

    /// <summary>
    /// Provides access to the settings in a settings container.
    /// </summary>
    public sealed class ApplicationDataContainerSettings : IPropertySet, IObservableMap<string, object>, IDictionary<string,object>, IEnumerable<KeyValuePair<string,object>>
    {
        internal ApplicationDataContainerSettings()
        {
#if WINDOWS_PHONE
            applicationSettings = System.IO.IsolatedStorage.IsolatedStorageSettings.ApplicationSettings;
            Microsoft.Phone.Shell.PhoneApplicationService.Current.Deactivated += Current_Deactivated;
            Microsoft.Phone.Shell.PhoneApplicationService.Current.Closing += Current_Closing;
#elif __IOS__
            _defaults = NSUserDefaults.StandardUserDefaults;
#endif
        }

#if WINDOWS_PHONE
        private IsolatedStorageSettings applicationSettings;

        void rootFrame_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e)
        {
            applicationSettings.Save();
        }

        void Current_Deactivated(object sender, Microsoft.Phone.Shell.DeactivatedEventArgs e)
        {
            applicationSettings.Save();
        }

        void Current_Closing(object sender, Microsoft.Phone.Shell.ClosingEventArgs e)
        {
            applicationSettings.Save();
        }
#elif __IOS__
        private NSUserDefaults _defaults;
#endif

        #region IObservableMap<string,object> Members

        private void OnMapChanged(string key, CollectionChange change)
        {
            // applicationSettings.Save();

            if (MapChanged != null)
            {
                MapChanged(this, new MapChangedEventArgs(key, change));
            }
        }

        /// <summary>
        /// Occurs when the map changes.
        /// </summary>
        [CLSCompliant(false)]
        public event MapChangedEventHandler<string, object> MapChanged;

        #endregion

        #region IDictionary<string,object> Members

        /// <summary>
        /// Adds an item to the <see cref="ApplicationDataContainerSettings"/>. 
        /// </summary>
        /// <param name="key">The key of the item to add.</param>
        /// <param name="value">The item value to add.</param>
        public void Add(string key, object value)
        {
#if WINDOWS_PHONE
            if (value is DateTimeOffset)
            {
                DateTimeOffset offset = (DateTimeOffset)value;
                value = offset.UtcDateTime;
            }

            applicationSettings.Add(key, value);
#elif __IOS__
            this[key] = value;

            /*if (value == null)
            {
                _defaults.SetNilValueForKey(new NSString(key));
            }
            else
            {
                TypeCode code = Type.GetTypeCode(value.GetType());
                switch (code)
                {
                    case TypeCode.String:
                        _defaults.SetString(value.ToString(), key);
                        break;
                    case TypeCode.Int32:
                        _defaults.SetInt((int)value, key);
                        break;
                    case TypeCode.Double:
                        _defaults.SetDouble((double)value, key);
                        break;
                    case TypeCode.Single:
                        _defaults.SetFloat((float)value, key);
                        break;
                    case TypeCode.Boolean:
                        _defaults.SetBool((bool)value, key);
                        break;
                    case TypeCode.DateTime:
                        NSDate nsd = NSDate.FromTimeIntervalSince1970(((DateTime)value).Ticks);
                        _defaults.SetValueForKey(nsd, new NSString(key));
                        break;

                    default:
                        break;
                }
            }*/
#endif
            OnMapChanged(key, CollectionChange.ItemInserted);
        }

        /// <summary>
        /// Returns a value that indicates whether a specified key exists in the <see cref="ApplicationDataContainerSettings"/>.
        /// </summary>
        /// <param name="key">The key to check for in the <see cref="ApplicationDataContainerSettings"/>.</param>
        /// <returns>true if an item with that key exists in the <see cref="ApplicationDataContainerSettings"/>; otherwise, false. </returns>
        public bool ContainsKey(string key)
        {
#if WINDOWS_PHONE
            return applicationSettings.Contains(key);
#elif __IOS__
            // TODO: see if there is a more efficient way of checking the key exists
            return _defaults.ValueForKey(new NSString(key)) != null;
#endif
        }

        /// <summary>
        /// Gets an ICollection object containing the keys of the <see cref="ApplicationDataContainerSettings"/>.
        /// </summary>
        public ICollection<string> Keys
        {
            get
            {
                System.Collections.ObjectModel.Collection<string> genericKeys = new System.Collections.ObjectModel.Collection<string>();
#if WINDOWS_PHONE
                foreach (string key in applicationSettings.Keys)
                {
                    genericKeys.Add(key);
                }
#endif
                return genericKeys;
            }
        }

        /// <summary>
        /// Removes a specific item from the <see cref="ApplicationDataContainerSettings"/>.
        /// </summary>
        /// <param name="key">The key of the item to remove.</param>
        /// <returns>true if the item was removed, otherwise false.</returns>
        public bool Remove(string key)
        {
#if WINDOWS_PHONE
            bool removed = applicationSettings.Remove(key);
#elif __IOS__
            bool removed = true;
            _defaults.RemoveObject(key);
#endif
            if (removed)
            {
                OnMapChanged(key, CollectionChange.ItemRemoved);
            }

            return removed;
        }

        /// <summary>
        /// Returns a value that indicates whether a specified key exists in the <see cref="ApplicationDataContainerSettings"/>.
        /// If an item with that key exists, the item is retrieved as an out parameter.
        /// </summary>
        /// <param name="key">The key to check for in the <see cref="ApplicationDataContainerSettings"/>.</param>
        /// <param name="value">The item, if it exists.
        /// Contains null if the item does not exist in the <see cref="ApplicationDataContainerSettings"/>.</param>
        /// <returns>true if an item with that key exists in the <see cref="ApplicationDataContainerSettings"/>; otherwise, false.</returns>
        public bool TryGetValue(string key, out object value)
        {
#if WINDOWS_PHONE
            return applicationSettings.TryGetValue<object>(key, out value);
#elif __IOS__
            NSObject obj = _defaults.ValueForKey(new NSString(key));
            value = IOSTypeConverter.ConvertToObject(obj);
            return obj != null;
#endif
        }

        /// <summary>
        /// Gets an ICollection object containing the values of the <see cref="ApplicationDataContainerSettings"/>.
        /// </summary>
        public ICollection<object> Values
        {
            get
            {
                System.Collections.ObjectModel.Collection<object> genericValues = new System.Collections.ObjectModel.Collection<object>();
#if WINDOWS_PHONE
                foreach (object value in applicationSettings.Values)
                {
                    genericValues.Add(value);
                }
#endif
                return genericValues;
            }
        }

        /// <summary>
        /// Gets or sets the element value at the specified key index.
        /// </summary>
        /// <param name="key"></param>
        /// <returns>The element value at the specified key index.</returns>
        public object this[string key]
        {
            get
            {
#if WINDOWS_PHONE
                object value = applicationSettings[key];
                if (value is DateTime)
                {
                    DateTime dateTime = (DateTime)value;
                    value = new DateTimeOffset(dateTime);
                }
                
                return value;
#elif __IOS__
                NSObject obj = _defaults.ValueForKey(new NSString(key));
                return IOSTypeConverter.ConvertToObject(obj);
#endif
                
            }

            set
            {
#if WINDOWS_PHONE
                // temporary workaround while investigating datetimeoffset behaviour in isostore
                if (value is DateTimeOffset)
                {
                    DateTimeOffset offset = (DateTimeOffset)value;
                    value = offset.UtcDateTime;
                }

                if (applicationSettings.Contains(key))
                {
                    if (applicationSettings[key] != value)
                    {
                        applicationSettings[key] = value;
                        OnMapChanged(key, CollectionChange.ItemChanged);
                    }
                }
                else
                {
                    // if not present add a new value (matches RT behaviour)
                    Add(key, value);
                }
#elif __IOS__
                if (value == null)
                {
                    _defaults.RemoveObject(key);
                    //_defaults.SetNilValueForKey(new NSString(key));
                }
                else
                {
                    TypeCode code = Type.GetTypeCode(value.GetType());
                    switch (code)
                    {
                        case TypeCode.String:
                            _defaults.SetString(value.ToString(), key);
                            break;
                        case TypeCode.Int32:
                            _defaults.SetInt((int)value, key);
                            break;
                        case TypeCode.Double:
                            _defaults.SetDouble((double)value, key);
                            break;
                        case TypeCode.Single:
                            _defaults.SetFloat((float)value, key);
                            break;
                        case TypeCode.Boolean:
                            _defaults.SetBool((bool)value, key);
                            break;

                        default:
                            _defaults.SetValueForKey(IOSTypeConverter.ConvertToNSObject(value), new NSString(key));
                            break;
                    }
                }
#endif
            }
        }

        #endregion

        #region ICollection<KeyValuePair<string,object>> Members

        /// <summary>
        /// Adds a new key-value pair to the ApplicationDataContainerSettings. 
        /// </summary>
        /// <param name="item">The key-value pair to add.</param>
        public void Add(KeyValuePair<string, object> item)
        {
            Add(item.Key, item.Value);
        }

        /// <summary>
        /// Removes all related application settings.
        /// </summary>
        public void Clear()
        {
#if WINDOWS_PHONE
            applicationSettings.Clear();
#elif __IOS__
            _defaults.Init();
#endif
            OnMapChanged(null, CollectionChange.Reset);
        }

        /// <summary>
        /// Returns a value that indicates whether a specified key-value pair exists in the ApplicationDataContainerSettings.
        /// </summary>
        /// <param name="item">The key-value pair to check for in the ApplicationDataContainerSettings.</param>
        /// <returns></returns>
        public bool Contains(KeyValuePair<string, object> item)
        {
#if WINDOWS_PHONE
            if (applicationSettings.Contains(item.Key))
            {
                object value = applicationSettings[item.Key];
                if (value is DateTime)
                {
                    DateTime dateTime = (DateTime)value;
                    value = new DateTimeOffset(dateTime);
                }

                if (value == item.Value)
                {
                    return true;
                }
            }
#endif
            return false;
        }

        /// <summary>
        /// Copies the elements of the collection to an array, starting at a particular array index. 
        /// </summary>
        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from the collection.
        /// The Array must have zero-based indexing.</param>
        /// <param name="arrayIndex">The zero-based index in array at which copying begins. </param>
        public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Gets the number of elements contained in the collection.
        /// </summary>
        public int Count
        {
            get { 
#if WINDOWS_PHONE
                return applicationSettings.Count;
#elif __IOS__
                // TODO: fix to compile with 8.4
                return 0;// Convert.ToInt32(_defaults.k);
#endif
            }
        }

        /// <summary>
        /// Gets a value indicating whether the dictionary is read-only. 
        /// </summary>
        /// <value>true if the dictionary is read-only; otherwise, false.</value>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Removes a specific key-value pair from the <see cref="ApplicationDataContainerSettings"/>. 
        /// </summary>
        /// <param name="item">The key-value pair to remove.</param>
        /// <returns>true if the item was removed, otherwise false.</returns>
        public bool Remove(KeyValuePair<string, object> item)
        {
            return Remove(item.Key);
        }

        #endregion

        #region IEnumerable<KeyValuePair<string,object>> Members

        IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string,object>>.GetEnumerator()
        {
            return new ApplicationDataContainerEnumerator();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new ApplicationDataContainerEnumerator();
        }

        #endregion
    }

    internal sealed class ApplicationDataContainerEnumerator : System.Collections.Generic.IEnumerator<KeyValuePair<string, object>>
    {
#if WINDOWS_PHONE
        private System.IO.IsolatedStorage.IsolatedStorageSettings settings;
#elif __IOS__
        private NSUserDefaults _defaults;
#endif
        private System.Collections.IEnumerator keyEnumerator;

        internal ApplicationDataContainerEnumerator()
        {
#if WINDOWS_PHONE
            settings = System.IO.IsolatedStorage.IsolatedStorageSettings.ApplicationSettings;
            keyEnumerator = settings.Keys.GetEnumerator();
#elif __IOS__
            _defaults = NSUserDefaults.StandardUserDefaults;
            // TODO: temp fix for old xamarin version
            keyEnumerator = null;// _defaults.ToDictionary().Keys.GetEnumerator();
#endif     
        }

        public KeyValuePair<string, object> Current
        {
            get { 
#if WINDOWS_PHONE
                object val = settings[keyEnumerator.Current.ToString()];
#elif __IOS__
                object val = null;
                NSObject obj = _defaults.ValueForKey(new NSString(keyEnumerator.Current.ToString()));
                val = IOSTypeConverter.ConvertToObject(obj);
                            
#endif
                return new KeyValuePair<string, object>(keyEnumerator.Current.ToString(), val);
            }
        }

        object System.Collections.IEnumerator.Current
        {
            get { return this.Current; }
        }

        public bool MoveNext()
        {
            return keyEnumerator.MoveNext();
        }

        public void Reset()
        {
            keyEnumerator.Reset();
        }

        public void Dispose()
        {
            keyEnumerator.Reset();
        }
    }

    internal sealed class MapChangedEventArgs : IMapChangedEventArgs<string>
    {
        private string key;
        private CollectionChange change;

        internal MapChangedEventArgs(string key, CollectionChange change)
        {
            this.key = key;
            this.change = change;
        }

        #region IMapChangedEventArgs<string> Members

        CollectionChange IMapChangedEventArgs<string>.CollectionChange
        {
            get { return change; }
        }

        string IMapChangedEventArgs<string>.Key
        {
            get { return key; }
        }

        #endregion
    }

#if __IOS__
    internal static class IOSTypeConverter
    {
        public static object ConvertToObject(NSObject obj)
        {
            object val = null;

            if (obj != null)
            {
                if(obj is NSString)
                {
                    return obj.ToString();
                }

                if(obj is NSDate)
                {
                    return NSDateHelper.ToDateTimeOffset((NSDate)obj);
                }

                if(obj is NSUuid)
                {
                    return new Guid(((NSUuid)obj).GetBytes());
                }

                if(obj is NSDecimalNumber)
                {
                    return decimal.Parse(obj.ToString(), CultureInfo.InvariantCulture);
                }

                if(obj is NSNumber)
                {
                    var x = (NSNumber)obj;
                    switch(x.ObjCType)
                    {
                        case "c":
                            return x.BoolValue;
                        case "l":
                        case "i":
                            return x.Int32Value;
                        case "s":
                            return x.Int16Value;
                        case "q":
                            return x.Int64Value;
                        case "Q":
                            return x.UInt64Value;
                        case "C":
                            return x.ByteValue;
                        case "L":
                        case "I":
                            return x.UInt32Value;
                        case "S":
                            return x.UInt16Value;
                        case "f":
                            return x.FloatValue;
                        case "d":
                            return x.DoubleValue;
                        case "B":
                            return x.BoolValue;
                        default:
                            return x.ToString();
                    }
                }

                if (obj.GetType() == typeof(NSString))
                {
                    val = ((NSString)obj).ToString();
                }
                else if (obj.GetType() == typeof(NSDate))
                {
                    val = NSDateHelper.ToDateTimeOffset((NSDate)obj);
                }
            }

            return val;
        }

        public static NSObject ConvertToNSObject(object obj)
        {
            if(obj != null)
            {
                if(obj is Boolean)
                {
                    return NSNumber.FromBoolean((bool)obj);
                }
                else if (obj is Byte)
                {
                    return NSNumber.FromByte((byte)obj);
                }
                else if (obj is SByte)
                {
                    return NSNumber.FromSByte((sbyte)obj);
                }
                else if (obj is Int16)
                {
                    return NSNumber.FromInt16((short)obj);
                }
                else if (obj is Int32)
                {
                    return NSNumber.FromInt32((int)obj);
                }
                else if (obj is Int64)
                {
                    return NSNumber.FromInt64((long)obj);
                }
                else if (obj is UInt16)
                {
                    return NSNumber.FromUInt16((ushort)obj);
                }
                else if (obj is UInt32)
                {
                    return NSNumber.FromUInt32((uint)obj);
                }
                else if (obj is UInt64)
                {
                    return NSNumber.FromUInt64((ulong)obj);
                }
                else if (obj is Single)
                {
                    return NSNumber.FromFloat((float)obj);
                }
                else if (obj is Double)
                {
                    return NSNumber.FromDouble((double)obj);
                }
                else if (obj is string)
                {
                    return new NSString(obj.ToString());
                }
                else if(obj is Guid)
                {
                    return new NSUuid(((Guid)obj).ToByteArray());
                }
                else if(obj is DateTimeOffset)
                {
                    return NSDateHelper.FromDateTimeOffset((DateTimeOffset)obj);
                }
            }

            return null;
        }
    }

    [CLSCompliant(false)]
    public static class NSDateHelper
    {
        private static readonly DateTimeOffset ReferenceDate = new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero);

        public static DateTimeOffset ToDateTimeOffset(NSDate date)
        {
            if(date == NSDate.DistantPast)
            {
                return DateTimeOffset.MinValue;
            }
            else if(date == NSDate.DistantFuture)
            {
                return DateTimeOffset.MaxValue;
            }

            return ReferenceDate.AddSeconds(date.SecondsSinceReferenceDate);
        }

        public static NSDate FromDateTimeOffset(DateTimeOffset value)
        {
            if(value == DateTimeOffset.MinValue)
            {
                return NSDate.DistantPast;
            }
            else if(value == DateTimeOffset.MaxValue)
            {
                return NSDate.DistantFuture;
            }

            return NSDate.FromTimeIntervalSinceReferenceDate(value.Subtract(ReferenceDate).TotalSeconds);
        }
    }
#endif
}
